/**
 * @author Chris Wilcox
 * @author Preston Parrett
 * 
 * @date 2008/03/17
 * 
 * The Evolution program was written for UCSB's CS165b class.
 * 
 */

package evolution;


import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Panel;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;


@SuppressWarnings("serial")
public class World extends Panel {
	
	private Life[][] world;
	private int rows, cols;
	private int numPlants;
	private int numAnimals;
	private int yearNum;
	private float fiveYrAvg;
	
	public World() {
		// Just do some random world
		this(30,30);
		populateWorld(22,10);
		
	}
	
	public World(int rows, int cols) {
		
		this.numPlants = 0;
		this.numAnimals = 0;
		
		this.rows = rows;
		this.cols = cols;
		world = new Life[rows][cols];
		
		yearNum=0;
		
		initDraw();
	}
	
	/**
	 * Clear all the old organisms from the world and place new ones.
	 * @param numPlants the number of producers to place in the world
	 * @param numAnimals the number of consumers to place in the world
	 */
	public void populateWorld(int numPlants, int numAnimals) {
		if (rows*cols < numPlants+numAnimals) {
			throw new RuntimeException("Too many plants and animals!");
		}
		
		world = new Life[rows][cols];
		
		this.numPlants = numPlants;
		this.numAnimals = numAnimals;
		
		try {
			
			placeMany(new Producer(this,0,0).getClass(), numPlants, PLACE_IN_CLUMPS);
			placeMany(new Consumer(this,0,0).getClass(), numAnimals, PLACE_AT_RANDOM);
			
		} catch (Throwable e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Clear all the old organisms from the world and place new ones.
	 * @param numPlants the number of producers to place in the world
	 * @param animals the animals to place in the new world
	 */
	public void populateWorld(int numPlants, Consumer[] animals) {
		
		if (rows*cols < numPlants+animals.length) {
			throw new RuntimeException("Too many plants and animals!");
		}
		
		world = new Life[rows][cols];
		
		this.numPlants = numPlants;
		this.numAnimals = animals.length;
		
		try {
			
			placeMany(new Producer(null,0,0).getClass(), numPlants, PLACE_IN_CLUMPS);
			placeMany(animals, numAnimals, PLACE_AT_RANDOM);
			
		} catch (Throwable e) {
			e.printStackTrace();
		}
	}
	
	public static final int PLACE_IN_CLUMPS = 0;
	public static final int PLACE_AT_RANDOM = 1;
	
	/**
	 * This will place the specified type of life in the amount specified using the method
	 * specified. The type passed in must be able to accept location parameters in the
	 * form (int row, int col).
	 * 
	 * @param type the type of Life to place.
	 * @param numToPlace the number of Life to place in the world
	 * @param placementMethod how to place the objects, either PLACE_IN_CLUMPS or PLACE_AT_RANDOM
	 */
	public void placeMany(Object type, int numToPlace, int placementMethod) throws IllegalArgumentException, SecurityException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
		
		boolean randomLife = true;
		
		if (type instanceof Life[])
		{
			randomLife = false;
			numToPlace = ((Life[])type).length;
		}
		
		switch (placementMethod) {
			case PLACE_IN_CLUMPS:
				
				for (int i=0; i<numToPlace; )
				{
					int numClumps = 5;
					int maxClump = numToPlace/numClumps;
					int inThisClump = (int)(Math.random()*maxClump+1);
					int startspot[] = findASpace();
					for (int j=0; j<inThisClump; j++) {
						int spot[] = findSpaceNear(startspot[0],startspot[1]);
						
						if (randomLife) 
						{
							// This code uses introspection to construct the type of Life specified
							// in the 'type' argument. Type must extend 'Life'
							place(
									(Life)((Class<?>)type).getConstructor(this.getClass(), Integer.TYPE,Integer.TYPE) // Get the constructor with these parameters
									.newInstance(this,spot[0],spot[1]), // Create a new instance (Use the contstructor)
									spot[0],
									spot[1]);
						}
						else {
							place( ((Life[])type)[i], spot[0], spot[1] ) ;
						}
						i++;
					}
				}
				
				break;
			case PLACE_AT_RANDOM:
				
				for (int i=0; i<numToPlace; i++)
				{
					int spot[] = findASpace();
					// Uses introspection same as above
					if (randomLife) 
					{
						place((Life)((Class<?>)type).getConstructor(this.getClass(), Integer.TYPE,Integer.TYPE).newInstance(this,spot[0],spot[1]),spot[0],spot[1]);
					}
					else {
						place( ((Life[])type)[i], spot[0], spot[1] ) ;
					}
				}

				break;
		}
			
	}
	
	public void place(Life lifeform, int row, int col) {
		lifeform.resetLocation(row,col);
		world[row][col] = lifeform;
	}
	
	public void clearProducers(){
		for(int i=0; i<rows; i++){
			for(int j=0; j<cols; j++){
				if(world[i][j] instanceof Producer)
					remove(i,j);
			}
		}
	}
	
	public void remove(int row, int col) {
		if ( !(at(row,col) instanceof Wall) ) { 
			world[row][col] = null;
		}
	}
	
	
	public int[] findASpace() {
		int row, col;
		do {
            row = (int)(Math.random()*rows) + 1;
            col = (int)(Math.random()*cols) + 1;
         } while (at(row, col) != null);
		return new int[] {row,col};
	}
	
	/*
	 * This function is primarily derived from the
	 * "FindSpaceNear()" function of the GA example from
	 * http://math.hws.edu/xJava/GA/index.html
	 */
	public int[] findSpaceNear(int row, int col) {
		int r, c, dx, dy, ct;
	      ct = 0;
	      while (ct < 20) {
	         if (at(row,col) == null)
	            return new int[] { row, col };
	         else {
	               ct = ct + 1;
	               switch ((int)(Math.random()*4)) {
	                  case 0: 
	                     if (row > 1)
	                        row = row - 1;
	                     break;
	                  case 1: 
	                     if (row < rows)
	                        row = row + 1;
	                     break;
	                  case 2: 
	                     if (col > 1)
	                        col = col - 1;
	                     break;
	                  default: 
	                     if (col < cols)
	                        col = col + 1;
	               }
	          }
	      }
	      r = row;
	      c = col;
	      while (at(r,c) != null ) {

	    	  r = (int)(Math.random()*rows);
	    	  c = (int)(Math.random()*cols);
	    	  	    	  
	    	  /*
	    	 // Randomly choose a dx and dy from { -1, 1 }
	    	 do {
	            dx = (int)(Math.random()*3) - 1;
	            dy = (int)(Math.random()*3) - 1;
	         } while ((dx == 0) && (dy == 0));
	         
	         while ( at(r,c) != null) {
	            r = r + dx;
	            c = c + dy;
	            if ( at(r,c) instanceof Wall ) break;
	         }
	         */
	         
	         
	      }
	      
	      
	    	  
	    	  
	      return new int[] { r, c };
	}
	
	public Visible at(int row, int col) {
		if (row <= 0 || row >= rows-1 || col <= 0 || col >= cols-1) {
			return new Wall();
		}
		return world[row][col];
	}
	
	
	
	
	/**
	 * Do one complete iteration in time.
	 * 
	 * Each display of the iteration lasts for speed milliseconds
	 * 
	 * @param speed number of milliseconds to display 
	 */
	synchronized public void tick() {

		Stats.report("Number ticks", 1);
		
		for ( int i=0; i<rows; i++ ) {
			for ( int j=0; j<cols; j++) {
				if ( world[i][j] instanceof Consumer ) {
					((Consumer)world[i][j]).doSomethingInWorld();
				}
			}
		}
		
		repaint();
	}
	
	synchronized public void newYear(){
		
		if (yearNum > 10000){
			System.exit(1);
		}
		
		yearNum++;
		Stats.reportWithReset("Number years", yearNum);
		
		
		
		// Get the consumers
		Consumer[] animalsToBreed = new Consumer[this.numAnimals];
		int cnt=0;
		for ( int i=0; i<rows; i++ ) {
			for ( int j=0; j<cols; j++) {
				if ( world[i][j] instanceof Consumer ) {
					animalsToBreed[cnt] = (Consumer)world[i][j];
					cnt++;
				}
			}
		}
		
		// Create a new Selector
		Selector breeder = new Selector((float)0.5);

		// Show stats for each year for first 5 yrs then every 5yrs after
		if (yearNum <= 5)
			Stats.report("Avg. Score (Yr. "+ yearNum +")", (float)breeder.average(breeder.getScores(animalsToBreed)),1);			
		else if(yearNum % 5 == 0){
			Stats.report("Avg. Score (Yr. "+ yearNum +")", (float)(fiveYrAvg / 5),1);
			fiveYrAvg = 0;
		}
		else 
			fiveYrAvg += (float)breeder.average(breeder.getScores(animalsToBreed));
		
		
		//File f = new File("log")
		Stats.log("" + Integer.toString(yearNum) + "," + (float)breeder.average(breeder.getScores(animalsToBreed)) + "\n");
		// Breed them
		Consumer[] newGeneration = breeder.breed(animalsToBreed);
		
		// Reset everything
		Stats.report("Number Producer", Stats.RESET);
		Stats.report("Number Consumer", Stats.RESET);
		
		for (Consumer c : newGeneration) {
			c.resetFitness();
		}
		
		//Re-populate the world with the new animals
		populateWorld(this.numPlants,newGeneration);
		
	}
	
	
	///////////////////////////
	// THREAD STUFF 
	///////////////////////////
	
	
	
	
	////////////////////////////
	// Painting the World
	////////////////////////////
	public void initDraw() {
		setBackground(new Color(200, 240, 255));
	}
	
	public Dimension cellSize() {
		return new Dimension(getWidth()/cols,getHeight()/rows);
	}
	
	synchronized public void paint(Graphics g) {
		super.paint(g);
		
		g.setColor(Color.black);
		g.drawRect(0,0,getWidth()-1,getHeight()-1);
		
		for (int i=0; i<rows; i++) {
			for (int j=0; j<cols; j++) {
				if (world[i][j] != null) {
					world[i][j].draw(g, i, j, cellSize().width);
				}
			}
		}
	}
}
